import fs from "fs"
import mkdir from "./mkdir"
import color from "./color"
import https from 'https'
import http from 'http'
import Path from 'path'
import {createHash} from 'crypto'
import { exit } from "process"
const header = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.44"
}

let ipPool = require(Path.join(__dirname,"ipPool.json"))
let Type = require(Path.join(__dirname,"Content-Type.json"))
let Trim = (str = "")=>str.replace(/(^\s*)|(\s*$)/g, ""); 

export default async function download(url:any,{dir="",name="",headers = header}:any){
    let end = false,number = 1;
    let ur = url.match(/^htt.*?\/\/.*?\//g)?.[0].replace(/htt.*?\/\/|\//g,"")
    let Timeout:any;
    return new Promise((complete:any)=>{
        let monitor =  (req:any)=>{
            // 延时5秒钟
            return setTimeout(()=>{
                if(end) return
                if(number === 4 && ipPool[ur]){
                    // 使用ip再请求5次
                    url = url.replace(ur,ipPool[ur]+(url.startsWith("https")?":443":":80"))
                }else if(number >= 8 ){
                    // 分析下网址有没有再ip池
                    console.log("已请求多次，均未成功，请检查自身的网络质量");
                    complete()
                    return
                    // 如果是单线程现在就不需要监控
                }
    
                console.log(color("绿色","未响应，重新请求> "+url))
                // 增加请求次数
                number++;
                req.destroyed || req.destroy();
                start()
            },6000)
        }

        let start = ()=>{
            // console.log("发送请求",url)
            let req = htt(url).get(url,{headers},async function (res:any) {
                //请求成功 关闭延迟监控
                clearTimeout(Timeout)
                switch (res.statusCode){
                    case 404 :{
                        console.log("这是一个404链接");
                        complete()
                        break;
                    }
                    case 301 :
                    case 302 :{
                        // 表示这层响应成功
                        console.log(color("黄色","重定向"),color("绿色",res.headers.location),"\n")
                        // 需要重定向，解析出重定向的位置
                        download(res.headers.location,{dir,name,headers}).then(e=>complete(e))
                        break;
                    }
                    case 206:
                    case 200 :{

                        // 创建父级文件夹
                        //解析出文件大小以及文件名
                        let fileData:any = pars_file(res.headers,url);
                        let suffx = Type[res.headers["content-type"].split(";")[0]];

                        if(!name){
                            name = fileData.filename || Trim(Path.posix.basename(fileData.name?fileData.name:url.replace(/\?.*$/,""))) || ""
                            fileData.filename || name.endsWith(suffx) || (
                                 suffx && !/^.*?\..*?\/.+/.test(url.replace(/\?.*$/,"")) && (name+=suffx)
                            )
                        }
                        // 解析路径信息
                        let pathData:fileInfo = createAvailablename(Path.join(dir,name));

                        mkdir(pathData.dir);

                        // 开始解析出可以用的文件名
                        pathData.url = url;

                        // 用于合成临时文件路径的名称
                        pathData.md5Name = md5(pathData.name)
                        pathData.filepath = Path.join(pathData.dir,pathData.name+pathData.ext);
                        // 下载的临时存放名
                        pathData.path = Path.join(pathData.dir,"未完成 "+pathData.md5Name.substring(0,5)+".cdownload")
                        // 保存文件的大小
                        pathData.size = fileData.size;
                        // if(pathData.size === 0)console.log("这不是一个标准的下载文件，因此没有进度条");
                        // 如果是多线程就执行多线程下载
                        if(res.headers["accept-ranges"] === "bytes"){
                            // 支持多线程下载
                            multithread(pathData,{headers:headers,numberThreads:16}).then(e=>{
                                console.log();
                                // 下载完成，删除状态数据
                                fs.truncateSync(pathData.path,Number(pathData.size))
                                fs.renameSync(pathData.path,pathData.filepath)
                                // 修改文件名
                                complete(pathData.filepath);
                            })
                        }else{
                            // 单线程下载
                            singleThread(pathData,headers).then(e=>{
                                console.log();
                                fs.renameSync(pathData.path,pathData.filepath)
                                complete(pathData.filepath);
                            })
                        }
                    }
                }
            });
            req.on("error",(err:any)=>{});
            // 开始监控
            Timeout = monitor(req);
        }
        // 启动下载
        start();
    })

}


/**
 * 多线程下载
 * @param fileData 
 * @param state_path 
 * @param headers 
 * @returns 
 */
async function multithread(fileData:fileInfo,{headers,numberThreads=16}:any){
    let size = 0,bf=0;
    // 这个负责下载文件的状态
    let state:any = {}

    // 检测这个文件是否被下载过 是否拥有状态文件以及缓存文件
    if(fs.existsSync(fileData.path)){
        // 不需要再创建文件
        console.log(color("青色","检测到缓存，开始恢复下载"));
        try{
            // 导入状态文件
            state = await readState(fileData);
        }catch(err){
            console.log(color("红色","缓存导入失败，重新下载"))
            // 报错了,删除缓存和状态文件
            fs.unlinkSync(fileData.path)
            fs.writeFileSync(fileData.path,"")
            state = {
                main:{numberThreads},
                threads:{}
            };
        }
    }else{
        // 这是新文件
        fs.writeFileSync(fileData.path,"")
        state = {
            main:{numberThreads},
            threads:{}
        };
    }

    // 这个是分段数据的起点
    let start_location = 0;
    // 开始创建文件状态信息
    state.main.numberThreads === 1 || file_piecewise(fileData.size,state.main.numberThreads).forEach((e,index)=>{
        if(state.threads[index]){
            size+=state.threads[index].cumulative;
            // 继续下载
            // 起点=起点＋累计
            state.threads[index].start=start_location + state.threads[index].cumulative;
            // 终点不变，累计不变
        }else {
            // 新下载线程
            state.threads[index] = {
                start:start_location,
                end:e,
                cumulative:0,
                // 这部分数据是否已经被下载完成
                isComplete:false
            }
        }
        start_location = e + 1;
    })

    // 把线程的基本信息写入文件
    writeState(fileData,state)

    console.log(color("青色","开启多线程"))
    console.log(color("蓝色","开始下载："),color("绿色",fileData.url),"\n")


    process.on('SIGINT', function () {
        // 在程序结束前把数据写入文本
        writeState(fileData,state)  
        process.exit();
    });
    await Promise.all(Object.keys(state.threads).map(async (i:any,index:any)=>{
        i = state.threads[i];
        // 为真代表这部分数据已经下载完成，直接结束这个下载进程
        if(i.isComplete === true) return
        await dow(fileData.url,{headers:{...headers,"Range":"bytes="+i.start+"-"+i.end},path:fileData.path,state,code:index},(len:number,code:number)=>{
            // 接收到数据信息
            state.threads[code].cumulative+=len;
            writeState(fileData,state)  
            size+=len;
            bf = progress(fileData.size,size,bf);
        })
    }))
    return fileData.path;
}


/**
 * 单线程下载
 */
async function singleThread(fileData:any,headers:any){
    let size = 0,bf=0;
    console.log(color("蓝色","开始下载："),color("绿色",fileData.url),"\n")

    // 创建下载文件
    fs.writeFileSync(fileData.path,"")

    // 只能进行单线程下载
    await dow(fileData.url,{headers,path:fileData.path},(len:number)=>{
        size+=len;
        // 进度
        bf = progress(fileData.size,size,bf);
    })
    return fileData.path;
}

//下载进度显示
function progress(total:number,now:number,superpro:number):number{
    let mb = (num:number)=>Number(Number(num/1024/1024).toFixed(2));
    if(!total){
        // 总量等于0 说明不是一个正常的文件，无法显示进度，只能显示下载量
        process.stdout.write("\r"+color("蓝色","Download")+" "+mb(now)+"MB")
        return 0;
    }else{
        let percentage = Number((now/total*100).toFixed(0));
        if(superpro < percentage){
            process.stdout.write("\r"+color("蓝色","Download ")+progressLog(percentage)+" %"+percentage+"")
            return percentage
        }
    }
    return superpro
}


// 一个下载线程
function dow (url:any,{headers,path,state,code = 0,state_path}:any,call:any){
    // 零时起点
    let tem_start = state?.threads[code].start || 0;
    // 监控
    let meout = undefined;

    return new Promise((resolve:any)=>{
        let monitor = (req:any)=>{
            return setTimeout(()=>{
                req.destroyed || req.destroy();
                // 重新设置请求头
                headers["Range"] = `bytes=${tem_start}-${state?.threads[code].end}`;
                // 重新启动
                start_down(tem_start)
            },3000);
        }

        let start_down = (start:number)=>{
            let req = htt(url).get(url,{headers},(res:any)=>{
                let stream = fs.createWriteStream(path,{start,flags:"r+"});
                stream.on("error",er=>{console.log(er);exit()})
                res.pipe(stream).on("close",()=>{resolve()})
                res.on("data",(num:any)=>{
                    tem_start+=num.length;
                    // 关闭上一次的监控
                    clearTimeout(meout);
                    // 再启动监控
                    meout = monitor(req);
                    // 为真表示这段时间来数据了
                    call(num.length,code)
                })
                res.on("end",()=>{
                    // 结束监控
                    clearTimeout(meout)
                    state && (state.threads[code].isComplete = true);
                    // 执行回调
                    call(0,code)
                })
                // 出现错误不用管，定时检查程序自己会处理
                res.on("error",()=>{})
            })
            // 出现错误不用管，定时检查程序自己会处理
            req.on("error",(err)=>{})
            // 执行监控
            meout = monitor(req);
        }
        // 启动下载
        start_down(tem_start);
    })
}


//去除前后空格

let pars_file = function(obj:any,urls:string){
    let name = "";
    if(obj["content-disposition"]?.includes("filename=")){
        name = obj["content-disposition"]?.substring(obj["content-disposition"].indexOf("filename=")+9)
    }
   // 转为中文
   return {
       url:     urls,
       name:    url(name.replace(/["]/g,"")),
       size:    obj["content-length"] || 0,
       path:    ""
   }
}


// 转义url
function url(str:any){
    if (!str || !/%[A-Za-z0-9]{2}/.test(str)) { return str; }
    return Array.from(Buffer.from(str.match(/%[A-Za-z0-9]{2}/g)
        .join("").replace(/%/g, ""), "hex").toString())
        .reduce((par, e) => {
          //e如果是中文需要3对
          if(/[^\x00-\xff]/.test(e)){return par.replace(/(%[A-Za-z0-9]{2}){3}/, e)}
          return par.replace(/%[A-Za-z0-9]{2}/, e);
        }, str);
}


// 创建可用的文件名
function createAvailablename(filepath:string):any{
    // 解析出后缀
    let fileData = Path.parse(filepath)
    if(!fs.existsSync(filepath)) return fileData;
    let i = 2;
    while(true){
        if(!fs.existsSync(Path.join(fileData.dir,fileData.name+`(${i})`+fileData.ext))){
            // 这个文件名称可用
            return Path.parse(Path.join(fileData.dir,fileData.name+`(${i})`+fileData.ext))
        }
        i++;
    }
}


// 把文件分段 piecewise
function file_piecewise(size:any,num:any){
    size = Number(size)
    num = Number(num)
    let a = [];
    let b = Number((size/num).toFixed(0));
    for(let i = 1;i<=num;i++)
        a.push(b*i)
    a[a.length-1]+=size-(b*num)
    return a;
}



function htt(url:string){
    return url.startsWith("https")?https:http
}

// 写入json
function writeState(fileData:any,data:any){
    let w = fs.createWriteStream(fileData.path,{start:Number(fileData.size),flags:"r+"});
    w.write(JSON.stringify(data))
    w.close();
}

// 读取json文件
function readState(fileData:any):any{
    let r = fs.createReadStream(fileData.path,{start:Number(fileData.size)});
    let data = "";
    return new Promise((res,callerror)=>{
        r.on("data",a=>data+=a)
        r.on("end",_=>{
            try{
                res(JSON.parse(data))
            }catch(err){
                callerror(data)
            }
        })
    })    
}

function md5(data:string){
    return createHash("md5").update(String(data)).digest("hex")
}

function progressLog(num){
    let str = "";
    let strs = ""
    num = Number(Number(num/2).toFixed(0));
    let a = 50 - num;
    while(num>0){
        strs+="█";
        num--
    }
    while(a>0){
        str+="░";
        a--
    }
    return color("绿色",strs)+str;
}